The currently used ElGamal/AES protocol has a number of significant disadvantages and is one of the reasons for the relatively slow operation of the I2P network. The protocol discussed in the article is designed to increase the speed and reliability of the network and opens up new opportunities, including the transmission of streaming audio and video. Based on the Noise protocol and Signal messenger algorithms. Detailed description Here. Of particular note is that the new protocol can be used with existing I2P addresses in conjunction with ElGamal/AES. The article is devoted to the implementation in i2pd
Basically the same cryptography is used as in NTCP2: x25519 replaces ElGamal and AEAD/Chacha20/Poly1305 replaces AES. In addition, HKDF-SHA256 is added, which creates keys of 32 or 64 bytes in length and is available in OpenSSL version 1.1.1; earlier versions use their own implementation using HMAC-SHA256.
Unlike the ElGamal temporary public key, which is a random 256 bytes, the x25519 public key is a point on an elliptic curve, and an attacker can figure out whether 32 bytes are the x25519 key and draw certain conclusions. In order to prevent this, the public key undergoes a transformation called Elligator, which is complex in itself and deserves a separate article. How and why it works is described in detail. Here.
A practical implementation based on OpenSSL does not seem so complicated, however:
The same problem exists with NTCP2, there the public key x25519 is encrypted with AES with the router address as the key, but here the threat model is different, because the attacker is inside I2P and this method of hiding the key is not suitable.
I2P addresses exchange with each other I2NP messages of type 11 - Garlic (“garlic”), the contents of which are fully encrypted data, encrypted by the sender’s address and decrypted by the recipient’s address. The principles of operation are described in more detail Here. In relation to ElGamal/AES, the following main points should be noted::
The new protocol introduces the concept of a session between address encryption keys; the encryption key uniquely determines the address, but one address can have several keys, for example, during multihoming or during restart. There can only be one session between a key pair, sessions are bidirectional, when a packet is received, the session determines the sender. All packets, except the first one, begin with an 8-byte tag that associates the packet with a tagset and a session. Tags are not transmitted, a are calculated simultaneously on both sides of the session.
The packet is encrypted by AEAD/Chacha20/Poly1305, the encryption key is unique for each packet and is calculated on both sides based on the previous one using HKDF.
When a connection is established, two messages are exchanged and 4 pairs of x25519 keys are involved: two pairs of address encryption keys and two pairs of temporary keys. The server's public encryption key (Bob) is taken from the LeaseSet of its address, public temporary keys are transmitted along with messages, hidden using Elligator, the client's public encryption key (Alice) is transmitted as a separate encrypted block.
NewSession(NS) -----------> Bob
Alice < — NewSessionReply(NSR)
During the connection process, the key ck (chaining key) is calculated using the Noise protocol based on the calculated common key x25519 (shared secret), HKDF is used as the MixKey operation
After successful negotiation, the first tagset is created with ck as the key
Alice sends NS messages until she receives an NSR from Bob, the first NSR received determines the current session, if a session already exists, then only the contents of the packet are processed. A separate temporary tagset is created with each NS, intended exclusively for receiving and decrypting the NSR.
Bob creates a new session upon receiving the first NS, and continues to send NSRs with keys and tags until he receives the first message of the established session.
The unencrypted Garlic message in the new protocol has a different structure than in ElGamal/AES. Instead of "garlics", which essentially represent I2NP messages with instructions for their delivery, the new protocol uses blocks of different types, similar to NTCP2. Each block consists of a header with type and length, and content. The following block types are currently used:
The previously mentioned tagset is a generator of triples (number, tag, key) for each new package. The numbers are in order starting from 0. The number is transmitted as a two-byte number, so no more than 65535 tags can be generated in one tagset, after which a new tagset is required. In practice, a much smaller value is used, in particular in i2pd a new tagset is created after sending 4K packets.
A tagset starts with a DH_INITIALIZE operation based on the shared key and the result of the DH_INITIALIZE of the previous tagset.
Then SESSTAG_CONSTANT is calculated, which is used to calculate tags
If it is necessary to create a new tagset, the sender creates a new key x25519 and adds a Next Key block with a public key to the next packet, the recipient also creates a new key and sends Next Key in response. After successful negotiation, the parties begin to use the new tagset, otherwise they continue to send Next Key using the previous tagset.
The new protocol has already been implemented and is used in the latest I2P releases: 0.9.46 Java and 2.32.0 i2pd. To enable it, you should add the parameters i2cp.leaseSetType=3 and
i2cp.leaseSetEncType=0.4 to interact with addresses with both old and new encryption, or i2cp.leaseSetType=3 and i2cp.leaseSetEncType=4 only for new encryption. It is also possible to work together with encrypted LeaseSets with the i2cp.leaseSetType parameter=5.
New Cryptography and Elligator
Basically the same cryptography is used as in NTCP2: x25519 replaces ElGamal and AEAD/Chacha20/Poly1305 replaces AES. In addition, HKDF-SHA256 is added, which creates keys of 32 or 64 bytes in length and is available in OpenSSL version 1.1.1; earlier versions use their own implementation using HMAC-SHA256.
Unlike the ElGamal temporary public key, which is a random 256 bytes, the x25519 public key is a point on an elliptic curve, and an attacker can figure out whether 32 bytes are the x25519 key and draw certain conclusions. In order to prevent this, the public key undergoes a transformation called Elligator, which is complex in itself and deserves a separate article. How and why it works is described in detail. Here.
A practical implementation based on OpenSSL does not seem so complicated, however:
- Only half of the keys are suitable for conversion. Suitability is determined by computing the Legendre symbol
- A valid key can be converted to a 254-bit random sequence and reconstructed from it by inverse transformation
- Arbitrary 254 bits can be converted back to some key x25519
- In I2P, the high 2 bits are filled with random values up to the full 32 bytes
The same problem exists with NTCP2, there the public key x25519 is encrypted with AES with the router address as the key, but here the threat model is different, because the attacker is inside I2P and this method of hiding the key is not suitable.
End-to-end I2P encryption
I2P addresses exchange with each other I2NP messages of type 11 - Garlic (“garlic”), the contents of which are fully encrypted data, encrypted by the sender’s address and decrypted by the recipient’s address. The principles of operation are described in more detail Here. In relation to ElGamal/AES, the following main points should be noted::
- The first message is encrypted with the recipient's ElGamal public key from his LeaseSet. Along with it, an AES key and a set of 32-byte tags for subsequent messages are transmitted. ElGamal encryption and especially decryption are very slow
- Each tag is used once. The recipient first compares the first 32 bytes of the message with known tags, and, if found, uses the appropriate AES key to decrypt the rest of the message. Thus, when using a tag, the sender must be sure that the tag is known to the recipient, periodically send new tags and wait for confirmation of receipt. Therefore, quite often the situation arises that there are no more confirmed tags and you have to use ElGamal again
- The recipient does not know where the message came from
- The length of an AES encrypted message is always a multiple of 16 bytes and leaves a remainder of 2 for ElGamal encrypted
The new protocol introduces the concept of a session between address encryption keys; the encryption key uniquely determines the address, but one address can have several keys, for example, during multihoming or during restart. There can only be one session between a key pair, sessions are bidirectional, when a packet is received, the session determines the sender. All packets, except the first one, begin with an 8-byte tag that associates the packet with a tagset and a session. Tags are not transmitted, a are calculated simultaneously on both sides of the session.
The packet is encrypted by AEAD/Chacha20/Poly1305, the encryption key is unique for each packet and is calculated on both sides based on the previous one using HKDF.
i2p::crypto::HKDF (m_CurrentSymmKeyCK, nullptr, 0, "SymmetricRatchet", m_CurrentSymmKeyCK);
Establishing a connection
When a connection is established, two messages are exchanged and 4 pairs of x25519 keys are involved: two pairs of address encryption keys and two pairs of temporary keys. The server's public encryption key (Bob) is taken from the LeaseSet of its address, public temporary keys are transmitted along with messages, hidden using Elligator, the client's public encryption key (Alice) is transmitted as a separate encrypted block.
NewSession(NS) -----------> Bob
Alice < — NewSessionReply(NSR)
During the connection process, the key ck (chaining key) is calculated using the Noise protocol based on the calculated common key x25519 (shared secret), HKDF is used as the MixKey operation
i2p::crypto::HKDF (m_CK, sharedSecret, 32, "", m_CK);
After successful negotiation, the first tagset is created with ck as the key
First tagset
uint8_t keydata[64];
i2p::crypto::HKDF (m_CK, nullptr, 0, "", keydata); // keydata = HKDF(chainKey, ZEROLEN, "", 64)
// k_ab = keydata[0:31], k_ba = keydata[32:63]
auto receiveTagset = std::make_shared<RatchetTagSet>(shared_from_this ());
receiveTagset->DHInitialize (m_CK, keydata); // tagset_ab = DH_INITIALIZE(chainKey, k_ab)
receiveTagset->NextSessionTagRatchet ();
m_SendTagset = std::make_shared<RatchetTagSet>(shared_from_this ());
m_SendTagset->DHInitialize (m_CK, keydata + 32); // tagset_ba = DH_INITIALIZE(chainKey, k_ba)
m_SendTagset->NextSessionTagRatchet ();
Alice sends NS messages until she receives an NSR from Bob, the first NSR received determines the current session, if a session already exists, then only the contents of the packet are processed. A separate temporary tagset is created with each NS, intended exclusively for receiving and decrypting the NSR.
Bob creates a new session upon receiving the first NS, and continues to send NSRs with keys and tags until he receives the first message of the established session.
Garlic message structure
The unencrypted Garlic message in the new protocol has a different structure than in ElGamal/AES. Instead of "garlics", which essentially represent I2NP messages with instructions for their delivery, the new protocol uses blocks of different types, similar to NTCP2. Each block consists of a header with type and length, and content. The following block types are currently used:
- Garlic Clove - Contains an I2NP message with delivery instructions. Typically data or LeaseSet. Instructions for delivery in i2pd are not currently used and are ignored upon receipt, but are filled in upon sending for compatibility with Java routers. The I2NP header has been changed to reduce the amount of data transferred
- Next Key - used to agree on the keys of a new tagset
- ACK Request - request to confirm receipt of a message. Usually requested along with sending a new LeaseSet
- ACK - response to ACK request
- Padding is a block of random length 0-16 bytes. Always last
Tagsets and creating new keys
The previously mentioned tagset is a generator of triples (number, tag, key) for each new package. The numbers are in order starting from 0. The number is transmitted as a two-byte number, so no more than 65535 tags can be generated in one tagset, after which a new tagset is required. In practice, a much smaller value is used, in particular in i2pd a new tagset is created after sending 4K packets.
A tagset starts with a DH_INITIALIZE operation based on the shared key and the result of the DH_INITIALIZE of the previous tagset.
DH_INITIALIZE
// DH_INITIALIZE(rootKey, k)
uint8_t keydata[64];
i2p::crypto::HKDF (rootKey, k, 32, "KDFDHRatchetStep", keydata); // keydata = HKDF(rootKey, k, "KDFDHRatchetStep", 64)
memcpy (m_NextRootKey, keydata, 32); // nextRootKey = keydata[0:31]
i2p::crypto::HKDF (keydata + 32, nullptr, 0, "TagAndKeyGenKeys", m_KeyData.buf);
// [sessTag_ck, symmKey_ck] = HKDF(keydata[32:63], ZEROLEN, "TagAndKeyGenKeys", 64)
memcpy (m_SymmKeyCK, m_KeyData.buf + 32, 32);
Then SESSTAG_CONSTANT is calculated, which is used to calculate tags
SESSTAG_CONSTANT and tags
The actual tag calculation is a simple HKDF from the previous one
[sessTag_ck, tag] = HKDF(sessTag_ck, SESSTAG_CONSTANT, "SessionTagKeyGen», 64)
i2p::crypto::HKDF (m_KeyData.GetSessTagCK (), nullptr, 0, "STInitialization", m_KeyData.buf); // [sessTag_ck, sesstag_constant] = HKDF(sessTag_ck, ZEROLEN, "STInitialization", 64)
memcpy (m_SessTagConstant, m_KeyData.GetSessTagConstant (), 32);
The actual tag calculation is a simple HKDF from the previous one
[sessTag_ck, tag] = HKDF(sessTag_ck, SESSTAG_CONSTANT, "SessionTagKeyGen», 64)
If it is necessary to create a new tagset, the sender creates a new key x25519 and adds a Next Key block with a public key to the next packet, the recipient also creates a new key and sends Next Key in response. After successful negotiation, the parties begin to use the new tagset, otherwise they continue to send Next Key using the previous tagset.
The new protocol has already been implemented and is used in the latest I2P releases: 0.9.46 Java and 2.32.0 i2pd. To enable it, you should add the parameters i2cp.leaseSetType=3 and
i2cp.leaseSetEncType=0.4 to interact with addresses with both old and new encryption, or i2cp.leaseSetType=3 and i2cp.leaseSetEncType=4 only for new encryption. It is also possible to work together with encrypted LeaseSets with the i2cp.leaseSetType parameter=5.